package utility;

import java.util.LinkedList;
import java.util.List;

public class PrimeGenerator {

	private static final long FIRST_PRIME = 2l;
	private long target;
	private long[] arrayOfPrimes;
	private int indexOfLastPrimeFound = 0;
	private int run = 1;
	private int runsNeeded;
	private int maxSieveLength = (int) (Integer.MAX_VALUE / 7);
	private boolean[] primeSieve;
	private long primeSieveOffSet;

	public void generateUpTo(long target) {
		this.target = target;
		arrayOfPrimes = new long[(int) (target / Math.log(target) + target * 0.5)];
		if (primesCanBeExpected()) {
			successivelySievePrimes();
		}
	}

	private void successivelySievePrimes() {
		calculateNumberOfPrimeSieveSteps();
		runSieveSteps();
	}

	private void runSieveSteps() {
		while (run <= runsNeeded) {
			initPrimeSieve();
			runSieveStep();
			run++;
			primeSieveOffSet += maxSieveLength;
		}
	}

	private void runSieveStep() {
		for (int i = findNextTrueInSieveStartingAt(2); i < primeSieve.length; i = findNextTrueInSieveStartingAt(i + 1)) {
			markMultiplesAsNonPrime((int) (2 * i + primeSieveOffSet), (long) i
					+ primeSieveOffSet);
		}
		addNumbersNotDivisibleByPreviouslyFoundPrimesToPrimeList();
	}

	private int findNextTrueInSieveStartingAt(int searchStart) {
		for (int i = searchStart - 1; i < primeSieve.length; i++) {
			if (primeSieve[i]) {
				return i + 1;
			}
		}
		//TODO work out correct logic for the next line
		return primeSieve.length + 3;
	}

	private void addNumbersNotDivisibleByPreviouslyFoundPrimesToPrimeList() {
		for (int i = 0; i < primeSieve.length; i++) {
			if (primeSieve[i]) {
				if (notDivisibleByPreviousPrimes(i + primeSieveOffSet + 1)) {
					arrayOfPrimes[indexOfLastPrimeFound] = i + primeSieveOffSet
							+ 1;
					indexOfLastPrimeFound++;
				}
			}
		}
	}

	private boolean notDivisibleByPreviousPrimes(long l) {
		if (run >= 2) {
			int rootOfTarget = (int) Math.sqrt(target);
			for (int i = 0; arrayOfPrimes[i] <= rootOfTarget
					&& i < indexOfLastPrimeFound; i++) {
				if (DivisibilityChecker.firstEvenlyDividesSecond(l,
						arrayOfPrimes[i])) {
					return false;
				}
			}
		}
		return true;
	}

	private void markMultiplesAsNonPrime(int l, Long prime) {
		for (int i = l - 1; i < primeSieve.length; i += prime) {
			primeSieve[i] = false;
		}
	}

	private void initPrimeSieve() {
		setPrimeSieveLength();
		initSieveWithCorrectValues();
	}

	private void initSieveWithCorrectValues() {
		for (int i = 0; i < primeSieve.length; i++) {
			primeSieve[i] = true;
		}
		if (run == 1) {
			primeSieve[0] = false;
		}
	}

	private void setPrimeSieveLength() {
		if (run < runsNeeded
				|| DivisibilityChecker.firstEvenlyDividesSecond(target,
						maxSieveLength)) {
			primeSieve = new boolean[maxSieveLength];
		} else {
			primeSieve = new boolean[(int) ((target) % maxSieveLength)];
		}
	}

	private void calculateNumberOfPrimeSieveSteps() {
		runsNeeded = (int) Math.ceil((float) target / (float) maxSieveLength);
	}

	private boolean primesCanBeExpected() {
		return target >= FIRST_PRIME;
	}

	public List<Long> reportPrimes() {
		List<Long> listOfPrimes = new LinkedList<Long>();
		for (int i = 0; i < indexOfLastPrimeFound; i++) {
			listOfPrimes.add(arrayOfPrimes[i]);
		}
		return listOfPrimes;
	}

	public Long reportLastPrimeFound() {
		return arrayOfPrimes[indexOfLastPrimeFound - 1];
	}

	public void setMaxSieveLength(int i) {
		maxSieveLength = i;
	}

	public long getXthPrimeNumber(int targetIndex) {
		int approximateTargetNeededToReachANumberOfPrimes = (int) (1.15 * (targetIndex
				* Math.log(targetIndex) + 10));
		generateUpTo(approximateTargetNeededToReachANumberOfPrimes);
		return arrayOfPrimes[targetIndex - 1];
	}

}
